﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Media;
using Microsoft.VisualStudio.Language.StandardClassification;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Microsoft.VisualStudio.Utilities;

//http://blog.280z28.org/archives/2009/11/74/

//consider using http://buildstarted.com/2010/09/07/razor-parser-engine-for-the-razor-syntax-highlighter/

namespace T4EditorClassifier
{

   
    #region Classifier
    /// <summary>
    /// Classifier that classifies all text as an instance of the OrinaryClassifierType
    /// </summary>
    internal class T4TagClassifier : IClassifier
    {
        IClassificationType _classificationType;

        internal T4TagClassifier(IClassificationTypeRegistryService registry)
        {
            _classificationType = registry.GetClassificationType("T4TagClassifier");
            if (_classificationType == null)
                throw new InvalidOperationException("Classifier not found");
        }

        /// <summary>
        /// This method scans the given SnapshotSpan for potential matches for this classification.
        /// In this instance, it classifies everything and returns each span as a new ClassificationSpan.
        /// </summary>
        /// <param name="trackingSpan">The span currently being classified</param>
        /// <returns>A list of ClassificationSpans that represent spans identified to be of this classification</returns>
        public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span)
        {
            var openers = new[] { '@', '=','+' };
            if (span.IsEmpty)
                return Enumerable.Empty<ClassificationSpan>().ToList();
            var txt = span.GetText();
            var firstMatch = Regex.Match(txt, "(<#|#>)");
            if (firstMatch.Success == false)
                return Enumerable.Empty<ClassificationSpan>().ToList();
            //create a list to hold the results
            List<ClassificationSpan> classifications = new List<ClassificationSpan>();
            var start = firstMatch.Index;
            while (start >= 0)
            {
                SnapshotSpan openSpan;
                if (txt.Substring(start, 2) == "#>")
                {
                    var openSearchStart = span.Start.Add(start - 2);
                    if (FindMatchingOpenChar(openSearchStart, this, "<#", "#>", int.MaxValue, out openSpan))
                    {

                        classifications.Add(new ClassificationSpan(openSpan, _classificationType));

                        //add the close
                        classifications.Add(new ClassificationSpan(new SnapshotSpan(span.Snapshot, new Span(span.Start + start, 2)), _classificationType));

                    }
                }
                else
                {
                    SnapshotSpan closeSpan;
                    var startLength = txt.Length > start + 2 && openers.Contains(txt[start + 2]) ? 3 : 2;
                    var open=new Span(span.Start + start, startLength);
                    openSpan=new SnapshotSpan(span.Snapshot,open);
                    classifications.Add(new ClassificationSpan(openSpan,_classificationType));


                    var closeSearchStart = span.Start.Add(start + startLength);
                    var hasClose = FindMatchingCloseChar(closeSearchStart, this, "<#", "#>", int.MaxValue, out closeSpan);
                    if (hasClose == false)
                        break;
                    classifications.Add(new ClassificationSpan(closeSpan,
                                                                   _classificationType));
                    if (closeSpan.Start > span.Start + span.Length)
                        break;
                    start = closeSpan.Start - span.Start;
                }
                start = txt.IndexOf("<#", start);
            }

            return classifications;
        }

       

        internal static bool FindMatchingOpenChar(SnapshotPoint start, IClassifier aggregator, string open, string close, int maxLines, out SnapshotSpan pairSpan)
        {
            pairSpan = new SnapshotSpan(start, start);
            ITextSnapshotLine line = start.GetContainingLine();
            int lineNumber = line.LineNumber;
            int offset = start - line.Start - 1;

            // if the offset is negative, move to the previous line
            if (offset < 0)
            {
                lineNumber--;
                line = line.Snapshot.GetLineFromLineNumber(lineNumber);
                offset = line.Length - 1;
            }

            string lineText = line.GetText();

            int stopLineNumber = 0;
            if (maxLines > 0 && maxLines != int.MaxValue)
                stopLineNumber = Math.Max(stopLineNumber, lineNumber - maxLines);

            int closeCount = 0;
            var openers = new[] { '@', '=', };
            while (true)
            {
                while (offset >= 0)
                {
                    var current = lineText.Substring(0, offset);
                    //char currentChar = lineText[offset];
                    // TODO: is this the correct affinity

                    if (current.EndsWith(open))
                    {
                        if (closeCount > 0)
                        {
                            closeCount--;
                        }
                        else
                        {
                            var startLength = current.Length <= offset && openers.Contains(current[offset - open.Length]) ? 3 : 2;
                            pairSpan = new SnapshotSpan(line.Start + offset - open.Length, startLength);
                            Debug.Assert(pairSpan.GetText() == "<#");
                            return true;
                        }
                    }
                    // TODO: is this the correct affinity
                    else if (current.EndsWith(close) )
                    {
                        return false; // can't nest close tags
                    }

                    offset--;
                }

                // move to the previous line
                lineNumber--;
                if (lineNumber < stopLineNumber)
                    break;

                line = line.Snapshot.GetLineFromLineNumber(lineNumber);
                lineText = line.GetText();
                offset = line.Length - 1;
            }

            return false;
        }

        internal static bool FindMatchingCloseChar(SnapshotPoint start, IClassifier aggregator, string open, string close, int maxLines, out SnapshotSpan pairSpan)
        {
            pairSpan = new SnapshotSpan(start.Snapshot, 1, 1);
            ITextSnapshotLine line = start.GetContainingLine();
            string lineText = line.GetText();
            int lineNumber = line.LineNumber;
            int offset = start.Position - line.Start.Position + 1;
            int stopLineNumber = start.Snapshot.LineCount - 1;
            if (maxLines < 0 || maxLines == int.MaxValue)
                stopLineNumber = int.MaxValue;
            else
                stopLineNumber = Math.Min(stopLineNumber, lineNumber + maxLines);

            int openCount = 0;
            while (true)
            {
                while (offset + close.Length <= line.Length)
                {
                    var openLength = open != null ? open.Length : 0;
                    //char currentChar = lineText[offset];
                    var maxsubLength = Math.Min(lineText.Length - offset, Math.Max(openLength, close.Length));
                    var current = lineText.Substring(offset, maxsubLength);
                    // TODO: is this the correct affinity
                    if (current.StartsWith(close) )
                    {
                        if (openCount > 0)
                        {
                            openCount--;
                        }
                        else
                        {
                            pairSpan = new SnapshotSpan(start.Snapshot, line.Start + offset, close.Length);
                            return true;
                        }
                    }
                    // TODO: is this the correct affinity
                    else if (open.HasValue() && current.StartsWith(open))
                    {
                        return false; // can't nest T4 open tags
                    }

                    offset++;
                }

                // move on to the next line
                lineNumber++;
                if (lineNumber > stopLineNumber || lineNumber >= line.Snapshot.LineCount)
                    break;

                line = line.Snapshot.GetLineFromLineNumber(lineNumber);
                lineText = line.GetText();
                offset = 0;
            }

            return false;
        }
#pragma warning disable 67
        // This event gets raised if a non-text change would affect the classification in some way,
        // for example typing /* would cause the classification to change in C# without directly
        // affecting the span.
        public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged;
#pragma warning restore 67
    }
    #endregion //Classifier
}
